本來準備往TypeScript和React的方向走,但TypeScript愈挖愈深發現還有很多有趣的東西沒有認識,而且也有工作上碰過的東西(汗)。
先前的文章很多都是在說如何用TypeScript加強型別安全,但TypeScript不僅在型別的保護上非常強大,還有相當多樣的工具與方法可以從一個Type創造另一個Type,有的第一眼看還很複雜...
所以接下來會有幾天來看怎麼創造Types,不過今天的主題是創造Types的運算子們(operators)應該還不會太難(吧)。
Intersection types 其實應該可以和union types一起講的,因為有些文章認為這兩個延伸自數學集合論的 ⋂ (intersection) 和 ⋃ (union),所以會把兩種組合型別(compose types)一起講。
雖然英文名詞是相同的,個人覺得intersection type創造型別的邏輯和數學理論是完全不一樣的,也有文章用多個例子證明確實如此,總之還是專心來看TypeScript的intersection types。
Intersection types是用 &
運算子將兩種型別合併(merge)成一種型別,合併後的型別會包含原先這兩種型別的所有屬性,如範例中的Place型別:
interface Position {
x: number;
y: number;
}
interface Site {
postalCode: number;
address: string;
name: string;
}
type Place = Position & Site;
const raycaCafe: Place = {
x: 25.06,
y: 121.52,
postalCode: 10049,
address: "No. 24, Jinxi St, Zhongshan District, Taipei City",
name: "RAYCA COFFEE"
}
alert(raycaCafe.address);
可以看到Place型別是由Position和Site型別合併而來,且有兩個型別的所有屬性,所以使用Place型別時如果少了一個屬性就會報錯。
Intersection types有幾個特性和數學上的intersection相似:
不過使用intersection types有一點要特別注意,假設今天有兩個不同名稱的interface有同名但不同型別的屬性,這兩個interface不能合併成一個型別,這邊舉一個錯誤範例來看會發生什麼事:
interface Position {
x: number;
y: number;
}
interface Location {
x: string;
y: string;
}
type Point = Position & Location;
const aPoint: Point = {
x: 25.06, // error
y: 121.52, // error
}
TypeScript compiler給了以下錯誤訊息:
Type 'number' is not assignable to type 'never'.(2322)
即使反過來改成 Location & Position
仍然會得到相同的錯誤訊息。
原因在於,這樣intersect的結果會讓屬性x和y都是 string & number
型別,TypeScript語法裡不存在這種同時是 string
型別又是 number
型別,所以給出 nuver
型別為,而 never
型別變數只有 never
型別的值可以賦值,因此拋出錯誤。
Intersection types乍看和 interfece extends interface
有點類似;但不同的是,extends
除了可以讓interface延伸另一個interface的所有屬性之外,還能額外增加其他屬性。
而intersection types的優點則是可以「串聯其他運算子而不用創造另一個interface」,例如需要 (T&B)|K
來獲得某種屬性集合的時候,&
運算子會比interface方便許多;此外 &
運算子也確保只有 (T&B)
的所有屬性集合,不會有其他不屬於T或B的額外屬性。
有神人用了許多例子介紹intersection types的使用方式和特性,而且還用動畫呈現intersection之後的結果!有興趣的人可以參考他的文章看更深入的介紹。
對於熟悉JavaScript的人來說,typeof
應該是再熟悉不過的運算子,因此這裡不多贅述,只需要注意,使用TypeScript時要記得:「typeof
是型別運算子」。
任何有需要取得型別的時候,都可以善用 typeof
運算子,例如以下狀況:
const num: number = 0;
const numArray: Array<num> = []; // error
Array是generic型別,<>
角括號內要輸入型別而非變數值 num
,這時候就能擅用 typeof
運算子:
const num: number = 0;
const numArray: Array<typeof num> = []; // ok
keyof
是TypeScript另一個很好用的型別運算子,keyof
正如其名,可用來取得物件型別的屬性(鍵值,key)並聯合成一個 string 或 number literal union型別,譬如:
interface Person {
name: string;
age: number;
h: number;
w: number;
}
type P = keyof Person; // type P = "name" | "age" | "h" | "w";
這裡要留意的是, keyof
的使用方式和 typeof
不一樣,keyof
是取得物件 型別 的鍵值,並非物件本身,所以這裡如果寫成 keyof 物件變數
會是錯的。
若想直接透過變數值取得鍵值union型別,應該要和 typeof
一起使用:keyof typeof 物件變數
。
keyof
運算子常用來操作物件屬性的型別,例如收到格式不同的新資料需要創建一個跟既有資料相同型別的物件:
// 既有資料
const raycaCafe = {
x: 25.06,
y: 121.52,
address: "No. 24, Jinxi St, Zhongshan District, Taipei City",
name: "RAYCA COFFEE"
}
type Cafe = typeof raycaCafe;
type CafeKey = keyof Cafe;
// 轉換成既有資料型別的函式
const cafeKeys: CafeKey[] = ["x", "y", "address", "name"];
const setData = function<T, K extends keyof T>(Keys: K[], data: Array<any>, emptyObj: Object ) {
const linkObj = emptyObj as {
[key in K]: any; // Avoid compile error
}
Keys.forEach((elem, index) => {
linkObj[elem] = data[index];
});
return linkObj;
}
// 處理新資料
const padrinoArr = [25.05, 121.55, "CAFÉ PADRINO", "Taipei City, Songshan District, Guangfu N Rd, 175, 1F"];
let padrino = {};
setData<Cafe, CafeKey>(cafeKeys, padrinoArr, padrino);
console.log(padrino);
/*
{
"x": 25.05,
"y": 121.55,
"address": "CAFÉ PADRINO",
"name": "105016, Taipei City, Songshan District, Guangfu N Rd, 175, 1F"
}
*/
這個範例只是試著將 keyof
運用在物件鍵值的型別處理,網路上可以看到更多 keyof
和函式的使用範例好文。
另外 keyof
也很常拿來創建maped types,但maped types之後才會說明,所以今天只要先認識 keyof
運算子如何創建literal union型別就好。
參考資料
intersection-types @TypeScript Handbook
Typeof Type Operator @TypeScript Handbook
Keyof Type Operator @TypeScript Handbook
Using TypeScript Intersection Types Like a Pro
Understanding discriminated union and intersection types in TypeScript
Typescript - Tips & Tricks - keyof